package pers.lonestar.framework.utils.support.analysis.analyser;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackageAccess;
import org.apache.poi.openxml4j.opc.PackagePart;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.usermodel.XSSFRelation;
import org.slf4j.Logger;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;

import pers.lonestar.framework.utils.support.analysis.handler.SharedStringsTableHandler;
import pers.lonestar.framework.utils.support.analysis.handler.XlsxRowHandler;
import pers.lonestar.framework.utils.support.cache.Ehcache;
import pers.lonestar.framework.utils.support.cache.MapCache;
import pers.lonestar.framework.utils.support.cache.ReadCache;
import pers.lonestar.framework.utils.support.contexts.XlsxReadContext;
import pers.lonestar.framework.utils.support.exception.ExcelAnalysisException;
import pers.lonestar.framework.utils.support.metadata.read.ReadSheet;
import pers.lonestar.framework.utils.support.metadata.read.ReadWorkbook;
import pers.lonestar.framework.utils.support.utils.FileUtil;
import pers.lonestar.framework.utils.support.utils.SheetUtil;

public class XlsxSaxAnalyser implements ExcelReadExecutor {
    private static final Logger log = org.slf4j.LoggerFactory.getLogger(XlsxSaxAnalyser.class);
    private static final int MAX_MAP_CACHE_SIZE = 5000000;
    private static final int DEFAULT_MAX_EHCACHE_ACTIVATE_SIZE = 20;
    private static final String DISALLOW_DOCTYPE_DECL = "http://apache.org/xml/features/disallow-doctype-decl";
    private static final String EXTERNAL_GENERAL_ENTITIES = "http://xml.org/sax/features/external-general-entities";
    private static final String EXTERNAL_PARAMETER_ENTITIES = "http://xml.org/sax/features/external-parameter-entities";
    private final XlsxReadContext analysisContext;
    private List<ReadSheet> sheetList;

    public XlsxSaxAnalyser(XlsxReadContext analysisContext) {
        this.analysisContext = analysisContext;
    }

    public void parseXmlSource(InputStream inputStream, ContentHandler handler) {
        InputSource inputSource = new InputSource(inputStream);
        try {
            SAXParserFactory saxFactory = SAXParserFactory.newInstance();
            saxFactory.setFeature(DISALLOW_DOCTYPE_DECL, true);
            saxFactory.setFeature(EXTERNAL_GENERAL_ENTITIES, false);
            saxFactory.setFeature(EXTERNAL_PARAMETER_ENTITIES, false);
            SAXParser saxParser = saxFactory.newSAXParser();
            XMLReader xmlReader = saxParser.getXMLReader();
            xmlReader.setContentHandler(handler);
            xmlReader.parse(inputSource);
            inputStream.close();
        } catch (ExcelAnalysisException e) {
            throw e;
        } catch (Exception e) {
            throw new ExcelAnalysisException(e);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    throw new ExcelAnalysisException("Can not close inputstream");
                }
            }
        }
    }

    @Override
    public void execute() {
        try {
            initAnalysisResources();
            analysisContext.getParameterSheetDataList().parallelStream().forEach(parameterSheet -> {
                parameterSheet = SheetUtil.match(sheetList, parameterSheet);
                if (parameterSheet != null) {
                    if (log.isDebugEnabled()) {
                        log.debug("Start analyzing ReadSheet(Index: {}, Name: {})", parameterSheet.getSheetIndex(), parameterSheet.getSheetName());
                    }
                    parseXmlSource(parameterSheet.getInputStream(), new XlsxRowHandler(analysisContext, parameterSheet));
                    if (log.isDebugEnabled()) {
                        log.debug("ReadSheet(Index: {}, Name: {}) analysis has been completed", parameterSheet.getSheetIndex(), parameterSheet.getSheetName());
                    }
                }
            });
        } catch (ExcelAnalysisException e) {
            throw e;
        } catch (Exception e) {
            throw new ExcelAnalysisException(e);
        }
    }

    public void initAnalysisResources() throws Exception {
        OPCPackage pkg = readOpcPackage(analysisContext);
        analysisContext.getReadWorkbook().setOpcPackage(pkg);
        ArrayList<PackagePart> packageParts = pkg.getPartsByContentType(XSSFRelation.SHARED_STRINGS.getContentType());
        //SharedString不为空，则对其分析
        if (!packageParts.isEmpty()) {
            PackagePart sharedStingsTablePackagePart = packageParts.get(0);

            //TODO
            //初始化ReadCache到Context上下文中
            ReadCache readCache = initReadCache(sharedStingsTablePackagePart);
            readCache.init();
            analysisContext.getReadWorkbook().setReadCache(readCache);

            //解析SharedString.xml中共享字符串存储到ReadCache中
            analysisSharedStringTable(sharedStingsTablePackagePart.getInputStream());
        }

        //获取SheetX.xml对应输入流，供后续使用sheet索引提取使用
        XSSFReader xssfReader = new XSSFReader(pkg);
        sheetList = new ArrayList<>();
        XSSFReader.SheetIterator ite = (XSSFReader.SheetIterator) xssfReader.getSheetsData();
        int index = 0;
        if (!ite.hasNext()) {
            throw new ExcelAnalysisException("Can not find any sheet");
        }
        while (ite.hasNext()) {
            InputStream inputStream = ite.next();
            ReadSheet readSheet = new ReadSheet();
            readSheet.setSheetIndex(index);
            readSheet.setSheetName(ite.getSheetName());
            readSheet.setInputStream(inputStream);
            sheetList.add(readSheet);
            index++;
        }
        if (log.isDebugEnabled()) {
            log.debug("XlsxSaxAnalyser initialization has been complete");
        }
    }

    private void analysisSharedStringTable(InputStream sharedStringInputStream) {
        if (log.isDebugEnabled()) {
            log.debug("Start analyzing shared string xml file");
        }
        ContentHandler sharedStringsTableHandler = new SharedStringsTableHandler(analysisContext.getReadWorkbook().getReadCache());
        parseXmlSource(sharedStringInputStream, sharedStringsTableHandler);
        analysisContext.getReadWorkbook().getReadCache().putFinished();
        if (log.isDebugEnabled()) {
            log.debug("Shared strings have been cached in the ReadCache");
        }
    }

    private ReadCache initReadCache(PackagePart sharedStringsTablePackagePart) {
        long size = sharedStringsTablePackagePart.getSize();
        if (size < 0) {
            try {
                size = sharedStringsTablePackagePart.getInputStream().available();
            } catch (IOException e) {
                log.warn("Unable to get file size, default used MapCache");
                return new MapCache();
            }
        }
        if (size < MAX_MAP_CACHE_SIZE) {
            if (log.isDebugEnabled()) {
                log.debug("使用MapCache，大小为：{}", size);
            }
            return new MapCache();
        }
        if (log.isDebugEnabled()) {
            log.debug("使用EhCache，大小为：{}", size);
        }
        return new Ehcache(DEFAULT_MAX_EHCACHE_ACTIVATE_SIZE);
    }

    private OPCPackage readOpcPackage(XlsxReadContext analysisContext) throws Exception {
        ReadWorkbook readWorkbook = analysisContext.getReadWorkbook();
        if (readWorkbook.getFile() != null) {
            return OPCPackage.open(readWorkbook.getFile());
        } else if (readWorkbook.getInputStream() != null) {
            File tmpReadDir = FileUtil.createCacheTmpFile();
            File tmpReadFile = new File(tmpReadDir.getPath(), UUID.randomUUID() + ".xlsx");
            readWorkbook.setTmpFile(tmpReadFile);
            FileUtil.writeToFile(tmpReadFile, readWorkbook.getInputStream());
            return OPCPackage.open(tmpReadFile, PackageAccess.READ);
        } else {
            throw new ExcelAnalysisException("No excel file or inputstream was provided");
        }
    }
}
